package br.uniceub.fsw.arquitetura.persistencia.dao;

import java.io.Serializable;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.List;

import br.uniceub.fsw.arquitetura.persistencia.conexao.ConexaoAtual;
import br.uniceub.fsw.arquitetura.persistencia.exception.BancoDadosException;

/**
 * Classe base (mãe) de todas as DAOs. Possui as regras de implementação
 * (métodos abstratos) e operações básicas já implementadas.
 * 
 * @author Daniel Rochetti
 * @author Bruno Schifer Bernardi
 *
 * @param &lt;T&gt; Tipo da entidade (VO) do DAO.
 */
public abstract class BaseDAO<T> {

    /**
     * Lista de parâmetros usados nos comandos SQL.
     */
    private List<Object> parametros;

    /**
     * Insere a entidade com os dados informados no banco de dados.
     * @param entidade Entidade com os dados preenchidos.
     */
    public abstract void inserir(T entidade);

    /**
     * Altera a entidade com os dados informados no banco de dados.
     * @param entidade Entidade com os dados preenchidos.
     */
    public abstract void alterar(T entidade);

    /**
     * Exclui a entidade com os dados informados do banco de dados.
     * @param entidade Entidade com os dados preenchidos.
     */
    public abstract void excluir(T entidade);

    /**
     * Consulta a entidae por sua chave primária.
     * @param id Chave primária da entidade.
     * @return Resultado da consulta.
     */
    public abstract T consultarPorId(Serializable id);

    /**
     * Consulta todas as entidades desse tipo.
     * @return Lista com as entidades consultadas.
     */
    public abstract List<T> consultarPorExemplo(T exemplo);

    /**
     * Obtém uma conexão do pool ou a referência para uma conexão já
     * iniciada para o usuário (thread) corrente.
     * @return Connection - conexão com a fonte de dados.
     */
    protected Connection getConexao() {
        return ConexaoAtual.getConexao();
    }

    /**
     * Libera a conexão atual.
     * Atenção, a conexão só é realmente liberada caso não esteja participando
     * de uma transação.
     * @see ConexaoAtual#liberarConexao()
     */
    protected void liberarConexao() {
        ConexaoAtual.liberarConexao();
    }

    /**
     * Executa uma alteração (INSERT, UPDATE ou DELETE).
     * 
     * @param sql Comando SQL.
     * @param params Parâmetros.
     * @return {@code true} caso tenha executado com sucesso
     * ou {@code false} caso contrário.
     */
    protected boolean executarAlteracao(String sql, List<?> params) {

        try {
            PreparedStatement stmt = getConexao().prepareStatement(sql);
            atribuirParametros(stmt, params);
            boolean sucesso = stmt.execute();
            stmt.close();

            liberarConexao();

            return sucesso;
        } catch (SQLException e) {
            throw new BancoDadosException(e.getMessage(), e);
        }
    }

    /**
     * Executa uma inserção e retorna a chave (id) gerado, em caso exista
     * de chave gerada no banco de dados..
     * 
     * @param sql Comando SQL.
     * @param params Parâmetros.
     * @return Object - chave gerada ou {@code null} caso nada tenha
     * sido gerado.
     */
    protected Object executarInsercao(String sql, List<?> params) {

        try {
            PreparedStatement stmt = getConexao().prepareStatement(sql);
            atribuirParametros(stmt, params);
            stmt.execute();

            // Recupera a chave (id) gerada, caso exista
            Object idGerado = null;
            ResultSet idGeradoResultSet = stmt.getGeneratedKeys();
            if (idGeradoResultSet.next()) {
                idGerado = idGeradoResultSet.getObject(1);
            }

            // libera recursos
            idGeradoResultSet.close();
            stmt.close();
            liberarConexao();

            return idGerado;
        } catch (SQLException e) {
            throw new BancoDadosException(e.getMessage(), e);
        }
    }

    /**
     * @see #consultar(String, List)
     */
    protected List<T> consultar(String sql) {

        return consultar(sql, null);
    }

    /**
     * Executa uma consulta e monta a lista com os resultados (linhas) encontrados.
     * 
     * @param sql Consulta SQL.
     * @param params Parâmetros da consulta.
     * @return Lista com o resultado encontrado (ou lista vazia caso nada
     * tenha sido encontrado).
     */
    protected List<T> consultar(String sql, List<?> params) {

        try {
            ResultSet resultSet = executarConsulta(sql, params);

            List<T> resultado = transformarResultadoEmLista(resultSet);
            resultSet.close();
            liberarConexao();

            return resultado;
        } catch (SQLException e) {
            throw new BancoDadosException(e.getMessage(), e);
        }
    }

    /**
     * @see #consultarUm(String, List)
     */
    protected T consultarUm(String sql) {

        return consultarUm(sql, null);
    }

    /**
     * Executa uma consulta e retorna apenas um (no máximo) resultado.
     * 
     * @param sql Consulta SQL.
     * @param params Parâmetros da consulta.
     * @return T objeto (entidade) encontrado ou {@code null} caso nada
     * tenha sido encontrado.
     */
    protected T consultarUm(String sql, List<?> params) {

        try {
            ResultSet resultSet = executarConsulta(sql, params);
            T resultado = null;

            if (resultSet.next()) {
                resultado = extrairObjetoResultado(resultSet);
            }
            resultSet.close();
            liberarConexao();

            return resultado;
        } catch (SQLException e) {
            throw new BancoDadosException(e.getMessage(), e);
        }
    }

    /**
     * @see #contar(String, List)
     */
    protected int contar(String sql) {

        return contar(sql, null);
    }

    /**
     * Executa uma consulta to tipo {@code select count(*) from ...}
     * 
     * @param sql Consulta SQL.
     * @param params Parâmetros da consulta.
     * @return quantidade de registros encontrado.
     * @see #consultarValorNumerico(String, List)
     */
    protected int contar(String sql, List<?> params) {

        Number resultado = consultarValorNumerico(sql, params);
        return resultado == null ? 0 : resultado.intValue();
    }

    /**
     * Consulta um valor numérico. Método útil nas consultas que retornam
     * apenas um valor numérico, por exemplo: {@code select avg(valor) from...}
     * 
     * @param sql Consulta SQL
     * @param params Parâmetros da consulta.
     * @return Number resultado numérico ou {@code null} caso nada tenha
     * sido encontrado.
     */
    protected Number consultarValorNumerico(String sql, List<?> params) {

        try {

            ResultSet resultSet = executarConsulta(sql, params);
            Number resultado = null;

            if (resultSet.next()) {
                resultado = (Number) resultSet.getObject(0);
            }
            resultSet.close();
            liberarConexao();

            return resultado;
        } catch (SQLException e) {
            throw new BancoDadosException(e.getMessage(), e);
        }
    }

    /**
     * Executa uma consulta com a consulta (sql) informada e os parâmetros
     * informados.
     * 
     * @param sql Consulta.
     * @param params parâmetros da consulta.
     * @return ResultSet - resultado
     * @throws SQLException caso ocorra algum erro ao executar a consulta.
     */
    protected ResultSet executarConsulta(String sql, List<?> params)
            throws SQLException {

        PreparedStatement stmt = getConexao().prepareStatement(sql);
        atribuirParametros(stmt, params);
        return stmt.executeQuery();
    }

    /**
     * Atribui os valores contidos na lista informada ao comando a ser
     * executado no banco de dados.
     * 
     * @param stmt Comando (query) que será executado.
     * @param params Parâmetros do comando.
     * @throws SQLException Caso ocorra algum erro ao atribuir os parâmetros.
     */
    protected void atribuirParametros(PreparedStatement stmt, List<?> params)
            throws SQLException {

        if (params != null && !params.isEmpty()) {
            for (int i = 0; i < params.size(); i++) {
                stmt.setObject(i + 1, params.get(i));
            }
        }
    }

    /**
     * Cria uma lista de parâmetros baseado nos parâmetros informados
     * em sequência.
     * @param params Vários parâmetros informados.
     * @return Lista dos parâmetros.
     */
    protected List<?> criarParametros(Object... params) {
        if(parametros == null) {
            parametros = new ArrayList<Object>(params.length);
        } else {
            limparParametros();
        }

        if (params != null && params.length > 0) {
            for (int i = 0; i < params.length; i++) {
                parametros.add(params[i]);
            }
        }
        return parametros;
    }
    
    /**
     * Adiciona um parametro na lista de parametros.
     */
    protected void adicionarParametro(Object parametro) {
        if(parametros == null) {
            parametros = new ArrayList<Object>();
        }
        
        parametros.add(parametro);
    }

    /**
     * Limpar parametros.
     */
    protected void limparParametros() {
        if(parametros != null) {
            parametros.clear();
        }
    }

    /**
     * Recupera a lista de parametros a ser usada no comando SQL.
     */
    protected List<?> recuperarParametros() {
        return parametros;
    }

    /**
     * Transforma o resultado de uma consulta em uma lista de entidades.
     * 
     * @param rs Resultado da consulta.
     * @return Lista de entidades.
     * @throws SQLException caso ocorra algum erro ao extrair o resultado.
     * 
     * @see #extrairObjetoResultado(ResultSet)
     */
    protected List<T> transformarResultadoEmLista(ResultSet rs)
            throws SQLException {

        List<T> resultado = new ArrayList<T>();
        while (rs.next()) {
            resultado.add(extrairObjetoResultado(rs));
        }
        return resultado;
    }

    /**
     * Transforma uma linha do resultado de uma consulta em uma instância
     * da entidade (VO) específica.
     * 
     * @param rs Resultado de uma consulta.
     * @return
     * @throws SQLException
     */
    protected abstract T extrairObjetoResultado(ResultSet rs)
            throws SQLException;
    
    /**
     * Gera o código restritivo de SQL para um objeto de exemplo informado.
     * 
     * @param query Contrutor de SQL já contendo o código de projeção do SQL a gerar.
     * @param exemplo Objeto do tipo VO a ser utilizado como exemplo de pesquisa.
     */
    protected abstract void mapearQueryParaExemplo(ConstrutorSQL query, T exemplo);
}
